Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restore focus after closing modal #1735

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

chenxsan
Copy link
Contributor

@chenxsan chenxsan commented Dec 28, 2022

Related issue #1370

cc @patrickhlauke if you don't mind please help review this one.

Basically the code would store the document.activeElement when modal is opened, and call focus on that stored document.activeElement when modal is closed.

I've added a button to the examples so we can test if the focus can be retained.

@netlify
Copy link

netlify bot commented Dec 28, 2022

Deploy Preview for docsearch canceled.

Name Link
🔨 Latest commit 715e120
🔍 Latest deploy log https://app.netlify.com/sites/docsearch/deploys/63aefd2b2e5af50008198490

@patrickhlauke
Copy link

patrickhlauke commented Dec 28, 2022

is there an easy way for me to check this live without having to work out how to run it locally?

from looking over the code itself, this looks correct though.

@shortcuts
Copy link
Member

Thanks a lot for the PR!

is there an easy way for me to check this live without having to work out how to run it locally?

For a concrete test you'd have to run de playground locally, it should only require installing deps and run yarn playground:start

Copy link
Member

@shortcuts shortcuts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this, would you mind adding a cypress test for this case?

packages/docsearch-react/src/DocSearch.tsx Show resolved Hide resolved
@chenxsan
Copy link
Contributor Author

Thanks a lot for the PR!

is there an easy way for me to check this live without having to work out how to run it locally?

For a concrete test you'd have to run de playground locally, it should only require installing deps and run yarn playground:start

I can see there's a netlify deploy preview, but it's canceled, any chance to make it work so anyone can test it online instead of running it locally?

@chenxsan
Copy link
Contributor Author

Thanks for tackling this, would you mind adding a cypress test for this case?

Sure, I'll add one.

@shortcuts
Copy link
Member

Thanks a lot for the PR!

is there an easy way for me to check this live without having to work out how to run it locally?

For a concrete test you'd have to run de playground locally, it should only require installing deps and run yarn playground:start

I can see there's a netlify deploy preview, but it's canceled, any chance to make it work so anyone can test it online instead of running it locally?

You can remove the ignore build command of the netlify.toml file, but I don't know if we have a button to re-focus on the website.

@chenxsan chenxsan marked this pull request as draft December 29, 2022 11:36
@chenxsan
Copy link
Contributor Author

Converted into draft until I figure out the cypress test here.

@chenxsan
Copy link
Contributor Author

chenxsan commented Dec 29, 2022

@shortcuts Seems we can't test this feature against the website as docusaurus customizes its search function based on DocSearch per https://github.com/facebook/docusaurus/blob/0985fa0af338068b4906f409a3f874a319f5359c/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx#L148.

Hence I would suggest running cypress against the example site, either react-example or js-example should be ok. Testing against a customized search function based on DocSearch where we have no control does't make sense to me.

@chenxsan
Copy link
Contributor Author

is there an easy way for me to check this live without having to work out how to run it locally?

from looking over the code itself, this looks correct though.

FYI, if you're going to run locally, you would need Node 16. 18 won't work as far as I see. Here's how you would run the project locally:

  1. clone
  2. run yarn
  3. run yarn build
  4. run yarn playground:start

This reverts commit 730f271.
@shortcuts
Copy link
Member

Hence I would suggest running cypress against the example site, either react-example or js-example should be ok. Testing against a customized search function based on DocSearch where we have no control does't make sense to me.

Makes a lot of sense indeed! Changing the website:test here https://github.com/algolia/docsearch/blob/main/package.json#L24 with playground:start should solve this, no?

@shortcuts
Copy link
Member

(again thanks a lot for the time spent here!)

examples/demo/src/App.js Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
@chenxsan chenxsan marked this pull request as ready for review December 30, 2022 13:08
@chenxsan
Copy link
Contributor Author

(again thanks a lot for the time spent here!)

It's my pleasure to improve a project I use every day :)

I think it's ready for review now, I've added some comments to explain why I made such change.

Copy link
Member

@shortcuts shortcuts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks a lot

cypress/integration/search/actions.spec.ts Show resolved Hide resolved
examples/demo/src/App.js Outdated Show resolved Hide resolved
examples/js-demo/app.js Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
packages/docsearch-react/src/DocSearch.tsx Outdated Show resolved Hide resolved
Copy link
Member

@shortcuts shortcuts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last comment!

packages/docsearch-react/src/DocSearch.tsx Outdated Show resolved Hide resolved
@chenxsan
Copy link
Contributor Author

@shortcuts
Copy link
Member

shortcuts commented Dec 30, 2022

Ooops, CI failed due to https://support.circleci.com/hc/en-us/articles/115014359648-Exit-Code-137-Out-of-Memory

oops D: you can update https://github.com/algolia/docsearch/blob/main/package.json#L18 to prevent building the playground, we don't need it on the CI anyway

I think adding --ignore @docsearch/(cypress|react-example|js-example) should do it

Copy link
Member

@francoischalifour francoischalifour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @chenxsan!

}

export function DocSearch(props: DocSearchProps) {
const searchButtonRef = React.useRef<HTMLButtonElement>(null);
const activeElementRef = React.useRef<Element>(document.body);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this working in server environments where document isn't defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I've never thought of rendering DocSearch in server side.

I guess it won't work.

Any suggestion?

Copy link
Contributor Author

@chenxsan chenxsan Dec 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this:

I've reran the tests locally, seems fine, though we need to apply type assertion for the initial object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the effect covered by the onOpen() callback?

I think we better have activeElementRef accept null as initial value, and update its value in the onOpen() and onClose() callbacks. This way, the effect can also be removed, and there's no document in the initial render path.

 export function DocSearch(props: DocSearchProps) {
   const searchButtonRef = React.useRef<HTMLButtonElement>(null);
+  const activeElementRef = React.useRef<Element | null>(null);
   const [isOpen, setIsOpen] = React.useState(false);
   const [initialQuery, setInitialQuery] = React.useState<string | undefined>(
     props?.initialQuery || undefined
   );

   const onOpen = React.useCallback(() => {
     setIsOpen(true);
+    activeElementRef.current = document.activeElement || document.body;
   }, [setIsOpen]);

   const onClose = React.useCallback(() => {
     setIsOpen(false);
+
+    if (activeElementRef.current) {
+      activeElementRef.current.focus();
+      activeElementRef.current = null;
+    }
   }, [setIsOpen]);

-  React.useEffect(() => {
-    if (
-      !isOpen &&
-      activeElementRef.current &&
-      activeElementRef.current instanceof HTMLElement
-    ) {
-      activeElementRef.current.focus();
-    }
-  }, [isOpen]);

   // ...
 }

Copy link
Contributor Author

@chenxsan chenxsan Dec 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this won't work,

   const onClose = React.useCallback(() => {
     setIsOpen(false);
+
+    if (activeElementRef.current) {
+      activeElementRef.current.focus();
+      activeElementRef.current = null;
+    }
   }, [setIsOpen]);

I believe I tried this in the beginning, and it would cause two tests to fail:

I.e., clicking the button to open dialog, then invoking shortcut to close the dialog would fail. I haven't yet figured out what could be wrong. Guess operating DOM right after calling setIsOpen(false) doesn't fit well with React. Introducing another useEffect would let React schedule the jobs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants